﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace BitmapTagger
{
	
	/// <summary>
	/// Represents the tags of a jpeg
	/// </summary>
	public class JpegTagger : BitmapTagger
	{
		

		/// <summary>
		/// The base of the query string used for exif queries
		/// </summary>
		private const string exifQueryBase = "/app1/ifd/exif/";

		/// <summary>
		/// The base of the query string used for ifd queries
		/// </summary>
		private const string ifdQueryBase = "/app1/ifd/";

		/// <summary>
		/// The base of the query string used for xmp queries
		/// </summary>
		private const string xmpQueryBase = "/xmp/";

		/// <summary>
		/// The end of the query string used for padding.
		/// </summary>
		private const string paddingSchemeQueryEnd = "PaddingSchema:Padding";


		/// <summary>
		/// Create a new jpeg tagger
		/// </summary>
		/// <param name="filePath">The file path of the jpeg image</param>
		/// <param name="loadExif">Whether or not to load Exif tags</param>
		/// <param name="loadXMPExif">Whether or not to load XMPExif tags</param>
		/// <param name="loadXMP">Whether or not to load XMP tags</param>
		/// <param name="precedence">The precedence of the tagging systems</param>
		public JpegTagger(string filePath, bool loadExif, bool loadXMPExif, bool loadXMP, 
			TagPrecedence precedence)
		{
			Precedence = precedence;
			Load(filePath, loadExif, loadXMPExif, loadXMP, Precedence);
		}

		/// <summary>
		/// Create a new JpegTagger
		/// </summary>
		/// <remarks>
		/// Defaults to load all tags
		/// Defaults Tagging System Precendence as Exif > XMPExif > XMP
		/// </remarks>
		/// <param name="filePath">The file path of the jpeg image</param>
		public JpegTagger(string filePath) : this(filePath, true, true, true, TagPrecedence.Exif_XMPExif_XMP) { }


		#region save


		/// <summary>
		/// Save the JPEG to the same file path it was loaded from
		/// </summary>
		/// <param name="saveExif">Whether or not to save the information to Exif tags</param>
		/// <param name="saveXMPExif">Whether or not to save the information to XMPExif tags</param>
		/// <param name="saveXMP">Whether or not to save the information to XMP tags</param>
		public override void Save(bool saveExif, bool saveXMPExif, bool saveXMP)
		{
			string tempFile = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + System.IO.Path.DirectorySeparatorChar + "temp.jpg";
			using (Stream inputFile = File.Open(FilePath, FileMode.Open, FileAccess.ReadWrite))
			{
				BitmapDecoder original = BitmapDecoder.Create(inputFile, BitmapCreateOptions.None, BitmapCacheOption.Default);
				BitmapMetadata metadata = original.Frames[0].Metadata.Clone() as BitmapMetadata;


				metadata.SetQuery("/app1/ifd/PaddingSchema:Padding", PaddingAmount);
				metadata.SetQuery("/app1/ifd/exif/PaddingSchema:Padding", PaddingAmount);
				metadata.SetQuery("/xmp/PaddingSchema:Padding", PaddingAmount);


				if (saveExif)
					SaveExif(metadata);

				if (saveXMPExif)
					SaveXMPExif(metadata);

				if (saveXMP)
					SaveXMP(metadata);

				//Recreate the image file with the new metadata
				JpegBitmapEncoder output = new JpegBitmapEncoder();
				output.Frames.Add(BitmapFrame.Create(original.Frames[0], original.Frames[0].Thumbnail, metadata, original.Frames[0].ColorContexts));
				//Must save to a temporary location as the stream to the original file is still open
				using (Stream outputFile = File.Create(tempFile))
				{
					output.Save(outputFile);
				}
			}

			//Move the temp file to be in place of the current file
			File.Copy(tempFile, FilePath, true);
			File.Delete(tempFile);

		}


		/// <summary>
		/// Save the JPEG 
		/// </summary>
		/// <param name="filePath">The file path to save to</param>
		/// <param name="saveExif">Whether or not to save the information to Exif tags</param>
		/// <param name="saveXMPExif">Whether or not to save the information to XMPExif tags</param>
		/// <param name="saveXMP">Whether or not to save the information to XMP tags</param>
		public override void Save(string filePath, bool saveExif, bool saveXMPExif, bool saveXMP)
		{
			if (!filePath.Equals(FilePath))
			{
				File.Copy(FilePath, filePath, true);
				FilePath = filePath;
			}

			Save(saveExif, saveXMPExif, saveXMP);
		}

		/// <summary>
		/// Save the JPEG with the tags to the location it was loaded from
		/// </summary>
		/// <remarks>
		/// When using Save() the thread it is called in must be marked at as a Single Thread Apartment (STA)
		/// Defaults as saving to all tagging systems (Exif, XMPExif, and XMP)
		/// </remarks>
		public override void Save()
		{
			Save(true, true, true);
		}


		/// <summary>
		/// Save the image to the specified file path, after the save
		/// the specified file path will be considered the file path of the 
		/// image it is working from`
		/// </summary>
		/// <remarks>
		/// When using Save the thread it is called in must be marked at as a Single Thread Apartment (STA)
		/// Defaults as saving to all tagging systems (Exif, XMPExif, and XMP)
		/// </remarks>
		/// <param name="filePath">The file path to have the image saved to</param>
		public override void Save(string filePath)
		{
			File.Copy(FilePath, filePath, true);
			FilePath = filePath;
			Save();
		}


		/// <summary>
		/// Save to the Exif tags
		/// </summary>
		/// <remarks>
		/// This will save each property to its corresponding Exif tag if an Exif tag is available for 
		/// the property
		/// </remarks>
		/// <param name="metadata">The metadata to save the tags to</param>
		private void SaveExif(BitmapMetadata metadata)
		{
			//YResolution
			if (YResolution != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.YResolutionID), YResolution.ToUInt64());
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.YResolutionID));

			//XResolution
			if (XResolution != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.XResolutionID), XResolution.ToUInt64());
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.XResolutionID));

			//DateTime
			if (DateTime != null)
				metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.DateTimeID), GetExifTagStringFromDateTime(DateTime));
			else
				metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.DateTimeID));

			//DateTime Original
			if (DateTimeOriginal != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.DateTimeOriginalID), GetExifTagStringFromDateTime(DateTimeOriginal));
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.DateTimeOriginalID));

			//DateTime Digitized
			if (DateTimeDigitized != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.DateTimeDigitizedID), GetExifTagStringFromDateTime(DateTimeDigitized));
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.DateTimeDigitizedID));

			//Image Description
			if (!String.IsNullOrEmpty(ImageDescription))
				metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.ImageDescriptionID), ImageDescription);
			else
				metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.ImageDescriptionID));


			//Make
			if (!String.IsNullOrEmpty(Make))
				metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.MakeID), Make);
			else
				metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.MakeID));


			//Model
			if (!String.IsNullOrEmpty(Model))
				metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.ModelID), Model);
			else
				metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.ModelID));

			//Software
			if (!String.IsNullOrEmpty(Software))
				metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.SoftwareID), Software);
			else
				metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.SoftwareID));

			//Artist
			if (!String.IsNullOrEmpty(Artist))
				metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.ArtistID), Artist);
			else
				metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.ArtistID));

			//Copyright
			if (!String.IsNullOrEmpty(Copyright))
				metadata.SetQuery(GetQueryStringForIFD(JpegExifIDs.CopyrightID), Copyright);
			else
				metadata.RemoveQuery(GetQueryStringForIFD(JpegExifIDs.CopyrightID));

			//UserComment
			if (!String.IsNullOrEmpty(UserComment))
			{
				BitmapMetadataBlob blob = new BitmapMetadataBlob(System.Text.ASCIIEncoding.ASCII.GetBytes(UserComment));
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.UserCommentID), blob);
			}
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.UserCommentID));

			//Metering Mode 
			if (MeteringMode != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.MeteringModeID), (ushort)MeteringMode);
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.MeteringModeID));

			//Maker Note
			if (!String.IsNullOrEmpty(MakerNote))
			{
				BitmapMetadataBlob blob = new BitmapMetadataBlob(System.Text.ASCIIEncoding.ASCII.GetBytes(MakerNote));
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.MakerNoteID), blob);
			}
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.MakerNoteID));

			//Exposure Program
			if (ExposureProgram != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.ExposureProgramID), ExposureProgram);
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.ExposureProgramID));

			//Exposure Time
			if (ExposureTime != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.ExposureTimeID), ExposureTime.ToUInt64());
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.ExposureProgramID));


			//ISO Speed Ratings
			if (ISOSpeedRatings != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.ISOSpeedRatingsID), ISOSpeedRatings);
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.ISOSpeedRatingsID));

			//Light Source
			if (LightSource != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.LightSourceID), LightSource);
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.LightSourceID));

			//FNumber
			if (FNumber != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.FNumberID), FNumber.ToUInt64());
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.FNumberID));

			//Shutter Speed
			if (ShutterSpeed != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.ShutterSpeedID), ShutterSpeed.ToInt64());
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.ShutterSpeedID));

			//Brightness
			if (Brightness != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.BrightnessID), Brightness.ToInt64());
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.BrightnessID));

			//Aperture
			if (Aperture != null)
				metadata.SetQuery(GetQueryStringForExif(JpegExifIDs.ApertureID), Aperture.ToUInt64());
			else
				metadata.RemoveQuery(GetQueryStringForExif(JpegExifIDs.ApertureID));
		}

		/// <summary>
		/// Save to the XMPExif tags
		/// </summary>
		/// <remarks>
		/// This will save each property to its corresponding XMPExif tag if an XMPExif tag is available for 
		/// the property
		/// </remarks>
		/// <param name="metadata">The metadata to save the tags to</param>
		private void SaveXMPExif(BitmapMetadata metadata)
		{
			//YResolution
			if (YResolution != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.YResolutionID, YResolution.ToString());
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.YResolutionID);

			//XResolution
			if (XResolution != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.XResolutionID, XResolution.ToString());
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.XResolutionID);

			//DateTime
			if (DateTime != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.DateTimeID, GetXMPTagStringFromDateTime(DateTime));
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.DateTimeID);

			//DateTime Original
			if (DateTimeOriginal != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.DateTimeOriginalID, GetXMPTagStringFromDateTime(DateTimeOriginal));
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.DateTimeOriginalID);


			//DateTime Digitized
			if (DateTimeDigitized != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.DateTimeDigitizedID, GetXMPTagStringFromDateTime(DateTimeDigitized));
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.DateTimeDigitizedID);

			//Image Description
			if (!String.IsNullOrEmpty(ImageDescription))
			{
				SetPrimaryValueForLangAlt(metadata,
					xmpQueryBase + JpegXmpIDs.ImageDescriptionID, ImageDescription);
			}
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ImageDescriptionID);

			//Make
			if (!String.IsNullOrEmpty(Make))
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.MakeID, Make);
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.MakeID);

			//Model
			if (!String.IsNullOrEmpty(Model))
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ModelID, Model);
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ModelID);

			//Software
			if (!String.IsNullOrEmpty(Software))
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.SoftwareID, Software);
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.SoftwareID);

			//Artist
			if (!String.IsNullOrEmpty(Artist))
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ArtistID, Artist);
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ArtistID);

			//Copyright
			if (!String.IsNullOrEmpty(Copyright))
			{
				SetPrimaryValueForLangAlt(metadata,
					xmpQueryBase + JpegXmpIDs.CopyrightID,
					Copyright);
			}
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.CopyrightID);

			//UserComment
			if (!String.IsNullOrEmpty(UserComment))
			{
				SetPrimaryValueForLangAlt(metadata,
					xmpQueryBase + JpegXmpIDs.UserCommentID, UserComment);
			}
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.UserCommentID);


			//Metering Mode 
			if (MeteringMode != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.MeteringModeID, MeteringMode.ToString());
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.MeteringModeID);

			//Exposure Program
			if (ExposureProgram != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ExposureProgramID, ExposureProgram.ToString());
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ExposureProgramID);


			//Exposure Time
			if (ExposureTime != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ExposureTimeID, ExposureTime.ToString());
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ExposureTimeID);


			//ISO Speed Ratings
			if (ISOSpeedRatings != null)
			{
				SetPrimaryValueForXMPSequence(metadata, 
					xmpQueryBase + JpegXmpIDs.ISOSpeedRatingsID, 
					ISOSpeedRatings);
			}
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ISOSpeedRatingsID);

			//Light Source
			if (LightSource != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.LightSourceID, LightSource.ToString());
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.LightSourceID);

			//FNumber
			if (FNumber != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.FNumberID, FNumber.ToString());
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.FNumberID);

			//Shutter Speed
			if (ShutterSpeed != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ShutterSpeedID, ShutterSpeed.ToString());
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ShutterSpeedID);

			//Brightness
			if (Brightness != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.BrightnessID, Brightness.ToString());
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.BrightnessID);

			//Aperture
			if (Aperture != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ApertureID, Aperture.ToString());
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ApertureID);
		}


		/// <summary>
		/// Save to the XMP tags
		/// </summary>
		/// <remarks>
		/// This will save each property to its corresponding XMP tag if an XMP tag is available for 
		/// the property
		/// </remarks>
		/// <param name="metadata">The metadata to save the tags to</param>
		private void SaveXMP(BitmapMetadata metadata)
		{

			//DateTime
			if (DateTime != null)
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.ModifyDateTimeID, GetXMPTagStringFromDateTime(DateTime));
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.ModifyDateTimeID);

			//Image Description
			if (!String.IsNullOrEmpty(ImageDescription))
			{
				SetPrimaryValueForLangAlt(metadata, 
					xmpQueryBase + JpegXmpIDs.DescriptionID, 
					ImageDescription);
			}
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.DescriptionID);

			//Software
			if (!String.IsNullOrEmpty(Software))
				metadata.SetQuery(xmpQueryBase + JpegXmpIDs.CreatorToolID, Software);
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.CreatorToolID);

			//Artist
			if (!String.IsNullOrEmpty(Artist))
			{
				SetPrimaryValueForXMPSequence(metadata, xmpQueryBase + JpegXmpIDs.CreatorID, Artist);
			}
			else
				metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.CreatorID);

			//Copyright
			if (!String.IsNullOrEmpty(Copyright))
			{
				SetPrimaryValueForLangAlt(metadata, 
					xmpQueryBase + JpegXmpIDs.RightsID, 
					Copyright);
			}
			else
			    metadata.RemoveQuery(xmpQueryBase + JpegXmpIDs.RightsID);
		}

		#endregion

		#region load


		/// <summary>
		/// Load a JPEG
		/// </summary>
		/// <param name="filePath">The file path of the JPEG to load</param>
		/// <param name="loadExif">Whether or not to load Exif tags</param>
		/// <param name="loadXMPExif">Whether or not to load XMPExif tags</param>
		/// <param name="loadXMP">Whether or not to load XMP tags</param>
		/// <param name="precendenceOfTags">The precendece of the tagging systems (used for conflicts)</param>
		public override void Load(string filePath, bool loadExif, bool loadXMPExif, bool loadXMP, 
			TagPrecedence precendenceOfTags)
		{
			Precedence = precendenceOfTags;
			Debug.Assert(ExtensionChecker.IsJPEGExtension(System.IO.Path.GetExtension(filePath)),
				"JpegTagger can only load a jpeg file");
			FilePath = filePath;


			using (Stream originalFile = File.Open(FilePath, FileMode.Open, FileAccess.Read))
			{
				BitmapDecoder originalImage = BitmapDecoder.Create(originalFile, BitmapOptions, BitmapCacheOption.None);
				var metadata = originalImage.Frames[0].Metadata.Clone() as BitmapMetadata;

				metadata.SetQuery(ifdQueryBase + paddingSchemeQueryEnd, PaddingAmount);
				metadata.SetQuery(exifQueryBase + paddingSchemeQueryEnd, PaddingAmount);
				metadata.SetQuery(xmpQueryBase + paddingSchemeQueryEnd, PaddingAmount);

				//Image Width  -- In JPEG this is held in a JPEG marker so we are obtaining it from
				//the JPEG decoder
				ImageWidth = (uint)originalImage.Frames[0].PixelWidth;
				
				//Image Height  --In JPEG this is held in a JPEG marker so we are obtaining it from
				//the JPEG decoder
				ImageHeight = (uint)originalImage.Frames[0].PixelHeight;

				if (precendenceOfTags == TagPrecedence.Exif_XMP_XMPExif)
				{
					if (loadXMPExif)
						LoadXMPExif(metadata);

					if (loadXMP)
						LoadXMP(metadata);

					if (loadExif)
						LoadExif(metadata);
				}
				else if (precendenceOfTags == TagPrecedence.Exif_XMPExif_XMP)
				{
					if (loadXMP)
						LoadXMP(metadata);

					if (loadXMPExif)
						LoadXMPExif(metadata);
					
					if (loadExif)
						LoadExif(metadata);
				}
				else if (precendenceOfTags == TagPrecedence.XMP_Exif_XMPExif)
				{
					if (loadXMPExif)
						LoadXMPExif(metadata);

					if (loadExif)
						LoadExif(metadata);

					if (loadXMP)
						LoadXMP(metadata);
				}
				else if (precendenceOfTags == TagPrecedence.XMP_XMPExif_Exif)
				{
					if (loadExif)
						LoadExif(metadata);

					if (loadXMPExif)
						LoadXMPExif(metadata);

					if (loadXMP)
						LoadXMP(metadata);
				}
				else if (precendenceOfTags == TagPrecedence.XMPExif_Exif_XMP)
				{
					if (loadXMP)
						LoadXMP(metadata);

					if (loadExif)
						LoadExif(metadata);

					if (loadXMPExif)
						LoadXMPExif(metadata);
				}
				else if (precendenceOfTags == TagPrecedence.XMPExif_XMP_Exif)
				{
					if (loadExif)
						LoadExif(metadata);

					if (loadXMP)
						LoadXMP(metadata);

					if (loadXMPExif)
						LoadXMPExif(metadata);
				}
				else
				{
					throw new Exception("Unknown TagPrecedence");
				}
			}
		}

		/// <summary>
		/// Load a JPEG
		/// </summary>
		/// <remarks>
		/// Defaults all tags to be loaded with a precedence of Exif > XMPExif > XMP
		/// </remarks>
		/// <param name="filePath">The file path of the JPEG to load</param>
		public override void Load(string filePath)
		{
			Debug.Assert(System.IO.Path.GetExtension(filePath).Equals(".jpg"), 
				"JpegTagger can only load a jpeg file");
			FilePath = filePath;

			Load(filePath, true, true, true, TagPrecedence.Exif_XMPExif_XMP);
		}

		/// <summary>
		/// Load the Exif tags
		/// </summary>
		/// <remarks>
		/// When loading a tag it will overwrite any previous loaded tag information
		/// <example>
		/// If the ExifXMP tag for Aperture has already been loaded and then the Exif tag for
		/// Aperture is loaded in this method, the Aperture will be assigned the value of the Exif tag
		/// </example>
		/// </remarks>
		/// <param name="metadata">The metadata the tags are contained in</param>
		private void LoadExif(BitmapMetadata metadata)
		{

			//XResolution
			if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.XResolutionID)))
				XResolution = new URational((UInt64)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.XResolutionID)));

			//YResolution
			if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.YResolutionID)))
				YResolution = new URational((UInt64)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.YResolutionID)));

			//Resolution Unit
			if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.ResolutionUnitID)))
				ResolutionUnit = (ushort?)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.ResolutionUnitID));

			//DateTime
			if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.DateTimeID)))
				DateTime = GetDateTimeFromExifString((string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.DateTimeID)));

			//Image Description
			if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.ImageDescriptionID)))
				ImageDescription = (string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.ImageDescriptionID));


			//Make
			if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.MakeID)))
				Make = (string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.MakeID));

			//Model
			if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.ModelID)))
				Model = GetStringFromPossibleSequence(metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.ModelID)));


			//Software
			if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.SoftwareID)))
				Software = (string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.SoftwareID));

			//Artist
			if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.ArtistID)))
				Artist = (string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.ArtistID));


			//Copyright
			if (metadata.ContainsQuery(GetQueryStringForIFD(JpegExifIDs.CopyrightID)))
				Copyright = (string)metadata.GetQuery(GetQueryStringForIFD(JpegExifIDs.CopyrightID));

			//UserComment
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.UserCommentID)))
			{
				BitmapMetadataBlob blob = (BitmapMetadataBlob)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.UserCommentID));
				UserComment = System.Text.Encoding.ASCII.GetString(blob.GetBlobValue());
			}


			//DataTimeOriginal
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.DateTimeOriginalID)))
				DateTimeOriginal = GetDateTimeFromExifString((string)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.DateTimeOriginalID)));


			//DataTimeDigitized
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.DateTimeDigitizedID)))
				DateTimeDigitized = GetDateTimeFromExifString((string)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.DateTimeDigitizedID)));

			//Metering Mode
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.MeteringModeID)))
				MeteringMode = (ushort?)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.MeteringModeID));

			//ManufactureNote
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.MakerNoteID)))
			{
				BitmapMetadataBlob blob = (BitmapMetadataBlob)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.MakerNoteID));
				MakerNote = System.Text.Encoding.Default.GetString(blob.GetBlobValue());
			}

			//Sensing Method
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.SensingMethodID)))
				SensingMethod = (ushort?)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.SensingMethodID));

			//ExposureProgram
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.ExposureProgramID)))
				ExposureProgram = (ushort?)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ExposureProgramID));

			//ExposureTime
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.ExposureTimeID)))
				ExposureTime = new URational((UInt64)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ExposureTimeID)));


			//ISO Speed Ratings
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.ISOSpeedRatingsID)))
				ISOSpeedRatings = (ushort?)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ISOSpeedRatingsID));

			//Light Source
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.LightSourceID)))
				LightSource = (ushort?)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.LightSourceID));


			//FNumber
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.FNumberID)))
				FNumber = new URational((UInt64)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.FNumberID)));

			//Shutter Speed
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.ShutterSpeedID)))
				ShutterSpeed = new Rational((Int64)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ShutterSpeedID)));

			//Brightness
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.BrightnessID)))
				Brightness = new Rational((Int64)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ShutterSpeedID)));

			//Aperture
			if (metadata.ContainsQuery(GetQueryStringForExif(JpegExifIDs.ApertureID)))
				Aperture = new URational((UInt64)metadata.GetQuery(GetQueryStringForExif(JpegExifIDs.ApertureID)));
		}


		/// <summary>
		/// Load the XMP tags
		/// </summary>
		/// <remarks>
		/// When loading a tag it will overwrite any previous loaded tag information
		/// <example>
		/// If the ExifXMP tag for ImageDescription has already been loaded and then the XMP tag for
		/// Description is loaded in this method, the Descirption will be assigned the value of the XMP tag
		/// </example>
		/// </remarks>
		/// <param name="metadata">The metadata the tags are contained in</param>
		private void LoadXMP(BitmapMetadata metadata)
		{

			//DateTime
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ModifyDateTimeID))
				DateTime = GetDateTimeFromXMPString((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ModifyDateTimeID));

			//Image Description
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.DescriptionID))
			{
				ImageDescription = 
					(string)GetPrimaryValueFromLangAlt(metadata, 
					xmpQueryBase + JpegXmpIDs.DescriptionID);
			}


			//Software
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.CreatorToolID))
				Software = (string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.CreatorToolID);

			//Artist
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.CreatorID))
			{
				Artist = (string)GetPrimaryValueFromXMPSequence(metadata, 
					xmpQueryBase + JpegXmpIDs.CreatorID); 
			}

			//Copyright
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.RightsID))
			{
				Copyright =
					(string)GetPrimaryValueFromLangAlt(metadata,
					xmpQueryBase + JpegXmpIDs.RightsID);
			}
		}


		/// <summary>
		/// Load the XMPExif tags
		/// </summary>
		/// <remarks>
		/// When loading a tag it will overwrite any previous loaded tag information
		/// <example>
		/// If the Exif tag for Aperture has already been loaded and then the XMPExif tag for
		/// Aperture is loaded in this method, the Aperture will be assigned the value of the XMPExif tag
		/// </example>
		/// </remarks>
		/// <param name="metadata">The metadata the tags are contained in</param>
		private void LoadXMPExif(BitmapMetadata metadata)
		{

			//XResolution
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.XResolutionID))
				XResolution = new URational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.XResolutionID));


			//YResolution
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.YResolutionID))
				YResolution = new URational(((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.YResolutionID)));


			//Resolution Unit
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ResolutionUnitID))
				ResolutionUnit = Convert.ToUInt16(metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ResolutionUnitID));

			//DateTime
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.DateTimeID))
				DateTime = GetDateTimeFromXMPString((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.DateTimeID));

			//Image Description
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ImageDescriptionID))
			{
				ImageDescription =
					(string)GetPrimaryValueFromLangAlt(metadata,
					xmpQueryBase + JpegXmpIDs.ImageDescriptionID);
			}


			//Make
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.MakeID))
				Make = (string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.MakeID);

			//Model
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ModelID))
				Model = GetStringFromPossibleSequence((metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ModelID)));


			//Software
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.SoftwareID))
				Software = (string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.SoftwareID);


			//Artist
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ArtistID))
				Artist = (string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ArtistID);


			//Copyright
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.CopyrightID))
			{
				Copyright = (string)GetPrimaryValueFromLangAlt(metadata,
					xmpQueryBase + JpegXmpIDs.CopyrightID);
			}


			//UserComment
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.UserCommentID))
			{
				UserComment =
					(string)GetPrimaryValueFromLangAlt(metadata,
					xmpQueryBase + JpegXmpIDs.UserCommentID);
			}


			//DataTimeOriginal
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.DateTimeOriginalID))
				DateTimeOriginal = GetDateTimeFromXMPString((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.DateTimeOriginalID));


			//DataTimeDigitized
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.DateTimeDigitizedID))
				DateTimeDigitized = GetDateTimeFromXMPString((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.DateTimeDigitizedID));

			//Metering Mode
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.MeteringModeID))
				MeteringMode = Convert.ToUInt16(metadata.GetQuery(xmpQueryBase + JpegXmpIDs.MeteringModeID));


			//Sensing Method
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.SensingMethodID))
				SensingMethod = Convert.ToUInt16(metadata.GetQuery(xmpQueryBase + JpegXmpIDs.SensingMethodID));
	
			//ExposureProgram
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ExposureProgramID))
				ExposureProgram = Convert.ToUInt16(metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ExposureProgramID));

			//ExposureTime
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ExposureTimeID))
				ExposureTime = new URational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ExposureTimeID));
	

			//ISO Speed Ratings
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ISOSpeedRatingsID))
			{
				object temp = GetPrimaryValueFromXMPSequence(metadata, 
					xmpQueryBase + JpegXmpIDs.ISOSpeedRatingsID);

				if (temp != null)
					ISOSpeedRatings = Convert.ToUInt16(temp);
			}
		
			//Light Source
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.LightSourceID))
				LightSource = Convert.ToUInt16(metadata.GetQuery(xmpQueryBase + JpegXmpIDs.LightSourceID));
		

			//FNumber
			if (metadata.Contains(xmpQueryBase + JpegXmpIDs.FNumberID))
				FNumber = new URational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.FNumberID));
		

			//Shutter Speed
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ShutterSpeedID))
				ShutterSpeed = new Rational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ShutterSpeedID));
		
			//Brightness
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.BrightnessID))
				Brightness = new Rational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.BrightnessID));

			//Aperture
			if (metadata.ContainsQuery(xmpQueryBase + JpegXmpIDs.ApertureID))
				Aperture = new URational((string)metadata.GetQuery(xmpQueryBase + JpegXmpIDs.ApertureID));

		}

		#endregion

		/// <summary>
		/// Get the query string used to access the passed exifID
		/// </summary>
		/// <param name="exifID">The exif ID to build the query string for</param>
		/// <returns>The query string for the BitmapMetadata queries</returns>
		private string GetQueryStringForExif(uint exifID)
		{
			return exifQueryBase + "{uint=" + exifID.ToString() +"}";
		}

		/// <summary>
		/// Get the query string used to access the passed IFD ID
		/// </summary>
		/// <param name="ID">The ID to build to the query string for</param>
		/// <returns>The query stirng for the BitmapMetadata queries</returns>
		private string GetQueryStringForIFD(uint ID)
		{
			return ifdQueryBase + "{uint=" + ID.ToString() + "}";
		}


		#region xmp data structure helpers

		/// <summary>
		/// The query string for the primary value in an xmp sequence
		/// </summary>
		private const string QUERY_STRING_FOR_PRIMARY_IN_SEQUENCE = "/{ulong=0}";

		/// <summary>
		/// The BitmapMetadata format value for an xmp sequence
		/// </summary>
		private const string XMP_SEQUENCE_FORMAT = "xmpseq";

		/// <summary>
		/// Get the primary value from a xmp sequence
		/// </summary>
		/// <param name="metadata">The metadata of the image</param>
		/// <param name="query">The query string to the xmp sequence</param>
		/// <returns>The value of the primary item in the xmp sequence, null if it does not exist</returns>
		private object GetPrimaryValueFromXMPSequence(BitmapMetadata metadata, string query)
		{
			BitmapMetadata sequenceMetaData = (BitmapMetadata)metadata.GetQuery(query);
			if (sequenceMetaData == null)
				return null;
			return sequenceMetaData.GetQuery(QUERY_STRING_FOR_PRIMARY_IN_SEQUENCE);
		}

		/// <summary>
		/// Set the primary value for an xmp sequence, will create the xmp sequence
		/// if it does not exist
		/// </summary>
		/// <param name="metadata">The metadata of the image</param>
		/// <param name="query">The query string to the xmp sequence</param>
		/// <param name="value">The value to save</param>
		/// <remarks>
		/// The value will be saved in its string format.
		/// Value cannot be null
		/// </remarks>
		private void SetPrimaryValueForXMPSequence(BitmapMetadata metadata, string query, object value)
		{
			Debug.Assert(value != null);
			if (metadata.ContainsQuery(query))
			{
				BitmapMetadata sequenceMetadata =
					(BitmapMetadata)metadata.GetQuery(query);
				sequenceMetadata.SetQuery(QUERY_STRING_FOR_PRIMARY_IN_SEQUENCE, value.ToString());
			}
			else
			{
				BitmapMetadata sequenceMetadata = new BitmapMetadata(XMP_SEQUENCE_FORMAT);
				sequenceMetadata.SetQuery(QUERY_STRING_FOR_PRIMARY_IN_SEQUENCE, value.ToString());
				metadata.SetQuery(query, sequenceMetadata);
			}
		}


		/// <summary>
		/// The ID for getting the default element in a Lang Alt elmenent
		/// </summary>
		public const string DEFAULT_LANG_ALT_ELEMENT_QUERY = "/x-default";

		/// <summary>
		/// The BitmapMetadata format value for a xmp lang alt
		/// </summary>
		private const string XMP_LANG_ALT_FORMAT = "xmpalt";

		/// <summary>
		/// Get the primary value from a xmp lang alt 
		/// </summary>
		/// <param name="metadata">The metadata of the image</param>
		/// <param name="query">The query string to the lang alt</param>
		/// <returns>The value of the primary item in the lang alt, null if it does not exist</returns>
		private object GetPrimaryValueFromLangAlt(BitmapMetadata metadata, string query)
		{
			BitmapMetadata langAltMetaData = (BitmapMetadata)metadata.GetQuery(query);

			if (langAltMetaData == null)
				return null;

			return langAltMetaData.GetQuery(DEFAULT_LANG_ALT_ELEMENT_QUERY);
		}


		/// <summary>
		/// Set the primary value for a xmp lang alt, will create the lang alt
		/// if it does not exist
		/// </summary>
		/// <param name="metadata">The metadata of the image</param>
		/// <param name="query">The query string to the lang alt</param>
		/// <param name="value">The value to save</param>
		/// <remarks>
		/// The value will be saved in its string format.
		/// Value cannot be null
		/// </remarks>
		private void SetPrimaryValueForLangAlt(BitmapMetadata metadata, string query, object value)
		{
			Debug.Assert(value != null);

			if (metadata.ContainsQuery(query))
			{
				BitmapMetadata langAltMetadata = (BitmapMetadata)metadata.GetQuery(query);
				langAltMetadata.SetQuery(DEFAULT_LANG_ALT_ELEMENT_QUERY, value.ToString());
			}
			else
			{
				BitmapMetadata langAltMetadata = new BitmapMetadata(XMP_LANG_ALT_FORMAT);
				langAltMetadata.SetQuery(DEFAULT_LANG_ALT_ELEMENT_QUERY, value.ToString());
				metadata.SetQuery(query, langAltMetadata);
			}
		}

		#endregion
	}
}
